CodePipelineからAWS FargateにサイドカーパターンのFireLensコンテナも含めBlue/Greenデプロイするパイプラインを作成するCloudFormationテンプレート
Fargateに素のNginx(Webサーバ)とFireLens(Fluent Bit)込みの検証環境を構築するCloudFormationのテンプレートを以前紹介しました。アプリコンテナ(Webサーバ)とFluent Bitのデプロイ作業を簡略化するためにCodePipelineでCI/CDパイプラインを作成し連携させてます。デプロイ方式はプレースホルダを用いたBlue/Green デプロイメントを採用します。
前回の環境おさらい
前回アプリコンテナは素のNginxコンテナを利用していました。FireLens用のFluent Bitのイメージは自前の設定ファイルを読み込ませる都合、ECRにビルドした設定ファイル込みのイメージをpushしたものを利用していました。
今回はプレースホルダを利用してBlue/Greenデプロイをするためappspec.yml
とtaskdef.json
が登場します。CodeBuildでイメージをビルドするためにbuildspec.yml
が必要になります。
今回使用するテンプレート
以下に置いてあります。
事前準備
CI/CDのCodePipelineを設定する前に準備が必要です。新登場する各種ファイル(appspec.yml
, taskdef.json
, buildspec.yml
)を保存するレポジトリが必要になります。
CodeCommitとECR作成
前回作成したECRと同じ名前のイメージレポジトリ作成するため前回のは一度削除してください。
rainコマンドで以下のパラメータで作成した環境を用います。
rain deploy ./1-CodeSeries.yml sample3-code-stack --params \ ProjectName=sample3,\ Environment=dev,\ CCRepositoryName1=webappservice,\ ECRRepositoryName1=custom-logrouter-firelens,\ ECRRepositoryName2=webapp
CodeCommitにソースコードと各種ファイルをPush
検証で使用したコードは以下に置いてあります。
taskdef.json
のアカウントIDは環境に合わせて修正してください。
], "family": "sample3-dev-webapp-taskdefinition", "taskRoleArn": "arn:aws:iam::012346789012:role/sample3-dev-webapp-ECSTaskRole", "executionRoleArn": "arn:aws:iam::0123456789012:role/sample3-dev-webapp-ECSTaskExecutionRole" }
その他全般ではsample3-dev-
とプレフィックを付けている箇所が多数あります。構築時のrainコマンド例通りの指定した場合は修正不要です。必要に応じてCloudFormationのパラメータで指定しているProjectName
, Environment
に合わせて適時修正してください。
CodeBuildの設定
Webアプリをビルドするためのbuildspec.yml
と、FireLensをビルドするためのbuildspec.yml
を用意しました。
. ├── README.md ├── appspec.yml ├── firelens │ ├── Dockerfile │ ├── buildspec.yml │ └── extra.conf ├── taskdef.json └── webapp ├── Dockerfile ├── buildspec.yml ├── go.mod ├── go.sum ├── handler │ └── handler.go └── main.go
WebアプリとFireLensではビルドする内容に違いがあるかもと思い分けて検証をはじめました。ほぼ同じ内容に落ち着きました。docker build
する対象のDockerfileのパスの指定が異なるだけです。
version: 0.2 env: variables: DOCKER_BUILDKIT: "1" phases: pre_build: commands: - echo Logging in to Amazon ECR... - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com - REPOSITORY_URI=${ACCOUNT_ID}.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/${REPOSITORY_NAME} - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8) build: commands: - echo Build started on `date` - docker build -t $REPOSITORY_NAME:$IMAGE_TAG ./webapp - docker tag $REPOSITORY_NAME:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG post_build: commands: - echo Build completed on `date` - echo Pushing the Docker image... - docker push $REPOSITORY_URI:$IMAGE_TAG - printf '{"ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json artifacts: files: imageDetail.json
${ACCOUNT_ID}
と、${REPOSITORY_NAME}
は環境変数を使っています。パイプライン作成のテンプレートで環境変数を渡しています。ポイントはCodeBuildに対する環境変数の設定ではなく、CodePipelineに対して環境変数を渡すところです。
環境変数の設定はCodePipelineのパイプラインに対して設定します。
CodeDeployの設定
Blue/Greenデプロイするためのappspec.yml
は1個です。CodePipelineの都合、Webアプリ、FireLens同時にビルドを行います。その後、同時にデプロイする方法しか取れませんでした。検討した他のパターンは最下部の「おわりに」で取り上げています。
つまり、Webアプリを修正すると変更のないFireLensも再度ビルドしてデプロイされます。逆もまた然りでFluent Bitの更新をかけると、アプリも再ビルド・再デプロイが行われます。
-
Icons made by Freepik from www.flaticon.com
appsepc.yml
の設定内容ではコンテナ名を変更していた場合に修正が必要です。
version: 0.0 Resources: - TargetService: Type: AWS::ECS::Service Properties: TaskDefinition: "<TASK_DEFINITION>" LoadBalancerInfo: ContainerName: "webapp" ContainerPort: 80 PlatformVersion: "1.4.0"
CodePipeline作成
テンプレート内で後々作成するデプロイグループ名を事前にsample3-dev-webservice-deploy-group
と指定しています。変更する場合は必要に応じてテンプレートを修正してください。
DeploymentGroupName: "sample3-dev-webservice-deploy-group"
rainコマンドで以下のパラメータで作成した環境を用います。
rain deploy ./3-CodePipeline.yml sample3-pipeline-stack --params \ ProjectName=sample3,\ Environment=dev,\ AppName1=webapp,\ AppName2=logrouter,\ PipelineName=webservice,\ S3BucketName1=artifact,\ CodeSeriesStackName=sample3-code-stack,\ AppECRName=sample3-webapp,\ FireLensECRName=sample3-custom-logrouter-firelens,\ CodeCommitRepositoryName=sample3-webappservice
パイプラインが作成されると初回は自動実行されます。初回は確実に失敗します。
Deployフェーズで失敗します。もしDeployフェーズ以外のエラーの場合、環境依存でテンプレートの修正が必要になるかと思われます。sample3-dev
と固定で入力している箇所が疑わしいです。
デプロイグループの手動作成
初回は確実に失敗する理由はCloudFormationがデプロイグループの作成に対応していなく必要なリソースが足りていません。テンプレートからすべて作成できませんでした。
以下リンクのLambdaでカスタムリソースでデプロイグループを作成していた意味を理解できました。
デプロイグループ以外の設定はCodePipelineテンプレートで作成してあります。テンプレート内で決め打ちしてデプロイグループ名を指定しています。今回はsample3-dev-webservice-deploy-group
の名前でデプロイグループを手動作成します。事前にデプロイグループ名を変更している場合は置き換えて読み進めてください。
手動設定箇所
CodeDepolyからアプリーケーションを開きます。
デプロイグループの作成します。
デプロイグループ名は事前に設定した名前を入力してください。サービスロール、他はテンプレートからリソースを作成済みです。統一感ある名前のリソースを次々に選択していくだけです。
本番稼働リスナーポートは80番、テストリスナーポートは8080番を想定して作成しています。
デプロイ設定はお好みで設定してください。ここではBlue/Greenデプロイ時、待機時間10分を設けた設定としました。また、古いタスクは1時間15分間待機し、すぐに切り戻せる設定としています。(本当は15分のつもりだったのですが、この後デプロイしたときに1時間15分と表示されて気づきました)
デプロイグループの手動作成は完了です。サービスロールも含め必要なリソースはテンプレートで作成されています。設定は選択するだけですので難しくはないと思います。似た名前のリソースが先にあったときは注意してください。
パイプラインテスト
テストのため手動で変更をリリースします。
Deployフェーズが実行中になりました。
指定した10分間トラフィックを新しいタスクへルーティングするか否かの待機中です。
Blue/Greenデプロイしている感がでてきました。実際にWebブラウザからアクセスして表示を確かめてみましょう。
アクセステスト
ELBのDNS名に対してWebブラウザからアクセスします。
Blue(本番環境: 既存タスク)
Nginxのデフォルトページが表示されます。既存環境は素のNginxコンテナをデプロイしているため期待通りです。
Green(テスト環境: 新タスク)
テスト環境はポート番号が8080
でルーティングされるよう設定しています。別の表示のWebページを確認できました。新タスクで起動している自前の設定を入れたWebコンテナの表示です。
ここまでの流れ
まずCodeCommitのレポジトリにGoのソースコードがあります。ソースコードをCodeBuildでビルドします。でき上がったバイナリファイルを別のイメージにコピーし、要はマルチステージビルドを行い最終的なできあがったイメージをECRにpushしています。CodeDeployがECRにある新しいイメージをFargateへBlue/Greenデプロイしました。テストリスナーポートを(8080
)通じて新しいイメージのコンテナへアクセスできているのが今の状態です。
本番環境へ切り替える
トラフィックの再ルーティングを押して、切り替えます。
すぐに切り替わりました。
80
番ポートでアクセスすると、自前の設定入りのWebコンテナの表示に切り替わっています。
8080
番ポートは変化なし。Nginxのデフォルトページが表示され、アクセス先のタスクが入れ替わると想定していたのですが違いました。気になったので後ほどターゲットグループの動きを確認します。
待機時間が長いので早々に元のタスク終了します。
Blue/Greenデプロイ完了です。
パイプラインも完了しています。
Blue/Greenの状態とターゲットグループ関係
デプロイ完了後8080
番ポートへアクセスすると80
番ポートと同じ表示がされたので設定を確認しました。リスナーの設定をみると80
も8080
も同じターゲットグループに向いています。であれば、同じアプリコンテナにアクセスしているのだから当然同じ表示になりますね。
改めてBlue/Greenデプロイを行いリスナーとターゲットグループの変化を確認します。まず、切り替え前のこの状態で確認します。
リスナーを確認すると...
次にトラフィックを切り替え後のこの状態で確認します。
リスナーをみると同じターゲットグループになっています。
最後にBlue/Greenデプロイが完了したこの状態で確認します。
リスナーをみるとトラフィック切り替え後の設定と変化なしです。
CodeCommitへのPushからの実行テスト
Webアプリの表示名を編集してからCodeCommitにPushすることでパイプラインが実行され、Blue/Greenデプロイができるか確認します。
Hello, 網走
をこんにちは、あばしり
に変更します。
package handler import ( "net/http" "github.com/labstack/echo/v4" ) func HomePage(c echo.Context) error { return c.String(http.StatusOK, "こんにちは、あばしり") } func HealthCheck(c echo.Context) error { return c.String(http.StatusOK, "Health Check OK") }
お行儀が悪いですがmain
ブランチにPushします。
$ git push origin main
main
ブランチの変更検知する設定はテンプレートでCloudWatch Eventsの設定を仕込んであります。
パイプラインの様子
main
ブランチの変更をCloudWatch Eventsで検知しパイプラインが走りはじめました。
また切り替えの状態まで来ました。
80
番ポートは以下の表示
8080
番ポートは以下のひらがな表示
トラフィックの再ルーティングをクリックすると、80
番ポートの表示はひらがな表示に切り替わりました。
CodeCommitの特定ブランチの変更をトリガーにしたBlue/Greenデプロイに成功を確認できました。
まとめ
- CloudFormationではCodeDeployのデプロイグループの作成がサポートされていないため、すべてテンプレート化はできない
- Lambdaでカスタムリソースを使う選択肢もある
- CloudFormation 一撃で Fargate の Blue/Green Deployment 環境を構築する | DevelopersIO
- マルチレポ構成であればCodePipelineは相性が良い
- モノレポ構成はCodePipelineからCodeBuild、CodeDeployを走らせる都合、全ビルド・全デプロイになるので相性が悪い
- GitHub Flowのような固定ブランチ名ならCodePipelineでトリガーに使えるので相性が良い
feature/*
のようにブランチ名が固定されていないものをトリガーにできない- CodePipelineからソースレポジトリのブランチの指定が固定ブランチのため
おわりに
Fargateの検証環境と、Fluent Bitの設定検証用途でCI/CDがたびたび必要になったためテンプレートにまとめました。と同時にCodePipelineでモノレポのような構成(アプリコンテナと、FireLensコンテナ)の場合、どうビルドして、どのようにデプロイするのか検証しました。この場合レポジトリにあるソースコードを全ビルド・全デプロイの構成をとるしかありませんでした。上手いことできないかと試してみたのですが簡単な手法は思いつきませんでした。
例えばマルチレポの様な構成でアプリ用のレポジトリ、Fluent Bit用のレポジトリと分けたとします。FireLensでサイドカーとしてFluent Bitが起動する都合、同じタスク内で共にコンテナとして起動します。双方のレポジトリから別々のパイプラインでデプロイしたくても、CodeDeployの仕様でECSのサービスにつき1つのデプロイグループしか関連付けられません。デプロイ方法が課題でこのパターンは断念しました。
デプロイグループ作成時のエラー
The ECS Service sample3-dev-service is already associated with the sample3-dev-webservice-deploy-group deployment group. An ECS Service can be associated with only one deployment group at a time.